import time
import os
import struct
import serial
import serial.tools.list_ports as list_ports



GET_VER = "M1005"
GET_EXECUTOR = "M1006"
GET_EXTEND = "M1010"
GET_FAULT = "M2000 S0"
GET_TEMP = "M105"
SET_TEMP_E = "M104 S"
SET_TEMP_B = "M140 S"
SAVE_SETTINGS = "M500"
HOME_ALL = "G28"
HOME_YZ = "G28 YZ"
GET_CHAMBER = "M1010"
REMAP_ADDON_PORT = "M1029 Y2 Z6"
REMAP_DEFAULT_PORT = "M1029 X3 Y4 Z5 B6 E1"
RESET_MACHINE = "M1999"

class DisconnectedException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class NoUSBstickException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class NoChamberException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class BootFailedException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class TestVersionException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class VersionException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class BedPortException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class NoExecutorException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class NoLinearException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class NoModelException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class ExecutorException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class ExtendException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class MeasureExctrException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class MeasureBedException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class MeasureTempException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class HeatBedException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class HeatExctrException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class HeatException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class SetTempException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class UpgradeException(RuntimeError):
    def __init__(self, message):
        RuntimeError.__init__(self, message)

class McSm2(object):
    NEW_LINE = int('\n'.encode('ascii')[0])

    READ_TIMEOUT  = 2
    WRITE_TIMEOUT = 5

    RET_OK              = 0
    RET_FAIL            = -1
    RET_TIMEOUT         = -2

    def __init__(self, port=None, ch=0, logger=None):
        if port == None or logger == None:
            raise Exception("please specify a serial port")

        self._port = port
        self._opened = False
        self._debug = False
        self._ch = ch
        self.logger = logger
        try:
            self.ser = serial.Serial(port=None, baudrate=115200, parity=serial.PARITY_NONE)
            self.ser.timeout = self.READ_TIMEOUT
            self.ser.write_timeout = self.WRITE_TIMEOUT
            self.ser.inter_byte_timeout = 0.5
        except Exception as e:
            raise Exception("failed to creat serial: {0}".format(e))

    def _open(self):
        if self._opened:
            self.logger.W("ch[{0}]: ".format(self._ch) + "MC: port is opened")
            return
        try:
            #self.ser = serial.Serial(port=self._port, baudrate=115200, parity=serial.PARITY_NONE)
            self.ser.port = self._port
            self.ser.open()
            self.ser.flushInput()
            self._opened = True
        except serial.SerialException as e:
            self.log_e("failed to open UART: {0}".format(e))
            raise

    def port(self, port=None):
        if port != None:
            self._port = port
        return self._port

    def ch(self, ch=None):
        if ch != None and ch < 4:
            self._ch = ch
        return self._ch

    def debug(self, new=None):
        if new != None:
            self._debug = new
        return self._debug

    def log_d(self, log=None):
        if log != None:
            self.logger.D("ch[{0}]: {1}".format(self._ch, log))

    def log_i(self, log=None):
        if log != None:
            self.logger.I("ch[{0}]: {1}".format(self._ch, log))

    def log_w(self, log=None):
        if log != None:
            self.logger.W("ch[{0}]: {1}".format(self._ch, log))

    def log_e(self, log=None):
        if log != None:
            self.logger.E("ch[{0}]: {1}".format(self._ch, log))

    def log_f(self, log=None):
        if log != None:
            self.logger.F("ch[{0}]: {1}".format(self._ch, log))


    def dump_packet(self, data, receive=True):
        if self._debug:
            self.log_d("%s: " % (("IN <-" if receive else "OUT->")))
            self.log_d(str(data, 'ascii'))

    def read_bytes_safe(self, num):
        data = self.ser.read(num)
        if len(data) != num:
            raise serial.SerialTimeoutException("ch[{0}]: ".format(self._ch) + "read timeout")
        return data

    def read_line(self):
        """
        read one line then return it
        read timeout is 2s
        """
        if not self._opened:
            raise serial.SerialException("ch[{0}]: ".format(self._ch) + "port is not opened")

        # read and check frame start magic
        packet = bytes()
        while True:
            try:
                packet += self.read_bytes_safe(1)
                if packet[-1] == self.NEW_LINE:
                    break
                elif "ok" in str(packet, 'ascii'):
                    packet += bytes([self.NEW_LINE])
                    break

            except serial.SerialTimeoutException:
                raise

            except serial.SerialException as e:
                self._opened = False
                self.ser.close()
                raise DisconnectedException(str(e))

        self.dump_packet(packet, receive=True)

        # payload only is returned
        return str(packet, 'ascii')

    def read_ack(self):
        lines = str()
        while True:
            try:
                ret = self.read_line()
                if "ok" in ret:
                    lines += ret
                    return [len(lines), lines]
                else:
                    lines += ret
            except serial.SerialTimeoutException as e:
                self.log_e("read_ack timeout")
                return [len(lines), lines]

    def write_packet(self, data):

        if not self._opened:
            raise serial.SerialException("ch[{0}]: ".format(self._ch) + "port is not opened")

        self.ser.flushInput()
        # frame start and direction magic
        packet = str(data).encode('ascii')
        # end code
        packet += bytes([self.NEW_LINE])

        try:
            self.ser.write(packet)
            self.ser.flush()

        except serial.SerialTimeoutException:
            self.log_e("write_packet timeout")
            raise serial.SerialTimeoutException("ch[{0}]: ".format(self._ch) + "write_packet timeout")

        except serial.SerialException as e:
            self._opened = False
            self.ser.close()
            raise DisconnectedException(str(e))

        self.dump_packet(packet, receive=False)
        return 0


    def send_cmd(self, cmd, read_back=True):
        cnt = 3
        while cnt > 0:
            try:
                if self.write_packet(cmd) == 0:
                    break
                else:
                    cnt -= 1
            except serial.SerialTimeoutException as e:
                self.log_e("send_cmd write timeout")
                cnt -= 1
                continue

        if cnt == 0:
            self.ser.close()
            self._opened = False
            raise DisconnectedException("ch[{0}]: ".format(self._ch) + "write timeout, close port")

        if read_back:
            return self.read_ack()

    def write(self, data):
        if not self._opened:
            raise serial.SerialException("ch[{0}]: ".format(self._ch) + "port is not opened")
        self.ser.write(data)

    def read(self, num):
        if not self._opened:
            raise serial.SerialException("ch[{0}]: ".format(self._ch) + "port is not opened")
        data = self.ser.read(num)
        if len(data) != num:
            return None
        return data

    def clear_unknown(self):
        self.ser.write_timeout = 1
        self.send_cmd(GET_VER)
        self.ser.write_timeout = self.WRITE_TIMEOUT

    def waiting_bootup(self):
        self.log_i("waiting boot")
        begin = time.time()

        self.ser.timeout = 5

        while time.time() - begin < 30:
            try:
                ret = self.send_cmd(GET_VER)
                if ret[0] == 0:
                    self.log_i("waiting boot")

                else:
                    if "Finish init" in ret[1] or "ok" in ret[1]:
                        self.log_i("MC is ready")
                        self.ser.timeout = self.READ_TIMEOUT
                        return ret
                    else:
                        self.log_i("waiting boot")

            except serial.SerialTimeoutException as e:
                self.log_e("waiting boot")
                time.sleep(1)

            except Exception as e:
                self.log_e("unexpected error in booting: {0}".format(e))
                raise

        self.ser.timeout = self.READ_TIMEOUT
        raise BootFailedException("ch[{0}]: ".format(self._ch) + "connect timeout, didn't boot up")

    def connect(self):
        self._open()

        self.waiting_bootup()

    def disconnect(self):
        if self._opened:
            self._opened = False
            self.ser.close()
        else:
            self.log_i("Port is closed!")

if __name__ == '__main__':
    mc = McSm2('/dev/usbserial1')
    mc.connect()
    mc.disconnect()